Rpmsg与Virtio介绍

您所在的位置:网站首页 virtio 驱动模型 Rpmsg与Virtio介绍

Rpmsg与Virtio介绍

2024-07-10 20:19:33| 来源: 网络整理| 查看: 265

Rpmsg与Virtio介绍

目录 Rpmsg与Virtio介绍 一、Rpmsg的介绍1、rpmsg_core.c的详细介绍1.1 rpmsg_bus结构体1.2 rpmsg_dev_match()函数1.3 rpmsg_dev_probe()函数1.4 rpmsg_register_device () 函数的介绍1.5 __register_rpmsg_driver() 函数的介绍1.6 rpmsg_create_ept、rpmsg_send、rpmsg_trysend、rpmsg_poll 2、rpmsg_char.c的详细介绍2.1 rpmsg_chrdev_driver结构体2.2 rpmsg_chrdev_probe()函数2.3 rpmsg_chrdev_remove()函数 二、virtio_rpmsg_bus.c 的详细介绍1、virtio_ipc_driver 结构体的介绍2、rpmsg_probe()函数的介绍3、__rpmsg_create_ept()介绍4、 virtio_endpoint_ops结构体介绍5、rpmsg_ns_cb() 函数的介绍6、rpmsg_create_channel() 函数的介绍7、virtio_rpmsg_ops() 结构体的介绍 三、Virtio的介绍1、virtio.c 的介绍1.1 virtio_bus结构体的介绍1.2 register_virtio_driver() 函数的介绍1.3 register_virtio_device() 函数的介绍1.4 virtio_dev_probe 函数的介绍1.5 virtio_dev_match 函数的介绍 2、virtio_input.c的介绍2.1 virtio_input_driver结构体介绍 四、virtio_device、virtio_driver、virtio_bus、rpmsg_device、rpmsg_drivver、rpmsg_bus如何如何联系且运作起来1、联系的建立2、运作

一、Rpmsg的介绍

源码所在路径:drivers\rpmsg\

Kconfig Makefile qcom_glink_native.c qcom_glink_native.h qcom_glink_rpm.c qcom_glink_smem.c qcom_smd.c rpmsg_char.c rpmsg_core.c rpmsg_internal.h virtio_rpmsg_bus.c

结合官方所提供的rpmsg framwork框架图,我们可知:

在这里插入图片描述

Rpmsg的整体框架是Rpmsg Bus、Rpmsg Device与Rpmsg Driver所构成,即Linux中的Bus模型;

Rpmsg Bus:由rpmsg_core.c文件所构建,负责

bus的构建;Driver-device的match;device的probe与remove;uevent机制; static struct bus_type rpmsg_bus = { .name = "rpmsg", .match = rpmsg_dev_match, .dev_groups = rpmsg_dev_groups, .uevent = rpmsg_uevent, .probe = rpmsg_dev_probe, .remove = rpmsg_dev_remove, };

Rpmsg Driver:由rpmsg_char.c所register(上图中还有两个文件,不在该章节进行介绍),负责:

register char driver;Remove char driver;该driver name 为 rpmsg_chrdev; static struct rpmsg_driver rpmsg_chrdev_driver = { .probe = rpmsg_chrdev_probe, .remove = rpmsg_chrdev_remove, .drv = { .name = "rpmsg_chrdev", }, };

Rpmsg Device:该层一开始是只有Virtio框架所构成的,后面添加了Glink与SMD架构(主要为高通所用),故主要介绍Virtio框架,通过上图可知其主要由virtio_rpmsg_bus.c 文件所维护:

该文件比较特殊,其是Virtio BUS与Rpmsg Bus的连接层,该文件中定义了virtio driver; static struct virtio_driver virtio_ipc_driver = { .feature_table = features, .feature_table_size = ARRAY_SIZE(features), .driver.name = KBUILD_MODNAME, .driver.owner = THIS_MODULE, .id_table = id_table, .probe = rpmsg_probe, .remove = rpmsg_remove, };

该virtio driver会向Rpmsg Bus register rpmsg device,这样一来Rpmsg Bus与Virtio Bus就通过该rpmsg device联系起来了。

1、rpmsg_core.c的详细介绍

在这里插入图片描述

1.1 rpmsg_bus结构体

结构体定义如下,可以看到该bus的名为rpmsg:

static struct bus_type rpmsg_bus = { .name = "rpmsg", .match = rpmsg_dev_match, .dev_groups = rpmsg_dev_groups, .uevent = rpmsg_uevent, .probe = rpmsg_dev_probe, .remove = rpmsg_dev_remove, };

该结构体会在rpmsg_init()中调用bus_register函数:

完成bus_type_private的初始化、创建并注册的这条总线需要的目录,该目录为rpmsg;在rpmsg目录下创建/device /driver 目录;初始化这条总线上的设备链表:struct klist klist_devices;初始化这条总线上的驱动链表:struct klist klist_drivers; static int __init rpmsg_init(void) { int ret; ret = bus_register(&rpmsg_bus); if (ret) pr_err("failed to register rpmsg bus: %d\n", ret); return ret; }

而该rpmsg_init函数会在注册进postcore_initcall,这样当kernel起来时就会调用根据initcall的顺序调用到rpmsg_init,进而注册Rpmsg Bus,为其准备所需要的资源。

postcore_initcall(rpmsg_init); 1.2 rpmsg_dev_match()函数

问:在Bus中,如何调入到Rpmsg Bus的match函数呢

答:当有driver/device register是会触发Bus中的bus_add_driver/bus_add_device继而触发Bus–>match/probe函数。

在该match函数中,匹配顺序如下:

static int rpmsg_dev_match(struct device *dev, struct device_driver *drv) { struct rpmsg_device *rpdev = to_rpmsg_device(dev); struct rpmsg_driver *rpdrv = to_rpmsg_driver(drv); const struct rpmsg_device_id *ids = rpdrv->id_table; unsigned int i; /* 针对特殊情况,dev中的driver_override被设置,则只匹配和driver_override名字相同的驱动程序 */ if (rpdev->driver_override) return !strcmp(rpdev->driver_override, drv->name); /* 后根据dev->id.mane与driver id_table中每个name进行匹配 */ if (ids) for (i = 0; ids[i].name[0]; i++) if (rpmsg_id_match(rpdev, &ids[i])) return 1; /* 最后以设备树进行匹配 */ return of_driver_match_device(dev, drv); } 1.3 rpmsg_dev_probe()函数

当Rpmsg Bus->match()成功后,则Bus会调用Rpmsg Bus->probe对driver与device进行bind

/* * when an rpmsg driver is probed with a channel, we seamlessly create * it an endpoint, binding its rx callback to a unique local rpmsg * address. * * if we need to, we also announce about this channel to the remote * processor (needed in case the driver is exposing an rpmsg service). */ static int rpmsg_dev_probe(struct device *dev) { struct rpmsg_device *rpdev = to_rpmsg_device(dev); struct rpmsg_driver *rpdrv = to_rpmsg_driver(rpdev->dev.driver); struct rpmsg_channel_info chinfo = {}; struct rpmsg_endpoint *ept = NULL; int err; /* 电源管理相关 */ err = dev_pm_domain_attach(dev, true); if (err) goto out; /* driver->callback存在会进行调用,但是rpmsg_char.c中的driver->callback == NULL * 此处可以客制化自己的driver */ if (rpdrv->callback) { /* 以device->id.name作为rpsmg_channel->name * channel的src为device->src */ strncpy(chinfo.name, rpdev->id.name, RPMSG_NAME_SIZE); chinfo.src = rpdev->src; chinfo.dst = RPMSG_ADDR_ANY; /* 会以回调的形式调用device->ops->create_ept,该ept->cb(rx_cb) =rpdrv->callback */ ept = rpmsg_create_ept(rpdev, rpdrv->callback, NULL, chinfo); if (!ept) { dev_err(dev, "failed to create endpoint\n"); err = -ENOMEM; goto out; } /* 把创建的ept和addr存储到device中 */ rpdev->ept = ept; rpdev->src = ept->addr; } /* 调用driver->probe == rpmsg_chrdev_probe */ err = rpdrv->probe(rpdev); if (err) { dev_err(dev, "%s: failed: %d\n", __func__, err); if (ept) rpmsg_destroy_ept(ept); goto out; } /* 调用device->ops->announce_create == virtio_rpmsg_announce_create */ if (ept && rpdev->ops->announce_create) err = rpdev->ops->announce_create(rpdev); out: return err; } 1.4 rpmsg_register_device () 函数的介绍 int rpmsg_register_device(struct rpmsg_device *rpdev) { struct device *dev = &rpdev->dev; int ret; dev_set_name(&rpdev->dev, "%s.%s.%d.%d", dev_name(dev->parent), rpdev->id.name, rpdev->src, rpdev->dst); rpdev->dev.bus = &rpmsg_bus; ret = device_register(&rpdev->dev); if (ret) { dev_err(dev, "device_register failed: %d\n", ret); put_device(&rpdev->dev); } return ret; } EXPORT_SYMBOL(rpmsg_register_device); 1.5 __register_rpmsg_driver() 函数的介绍 int __register_rpmsg_driver(struct rpmsg_driver *rpdrv, struct module *owner) { rpdrv->drv.bus = &rpmsg_bus; rpdrv->drv.owner = owner; return driver_register(&rpdrv->drv); } EXPORT_SYMBOL(__register_rpmsg_driver);C 1.6 rpmsg_create_ept、rpmsg_send、rpmsg_trysend、rpmsg_poll

从函数实体可以看到,这些operation都是以回调的方式调用,所以进行客制化的实现;

struct rpmsg_endpoint *rpmsg_create_ept(struct rpmsg_device *rpdev, rpmsg_rx_cb_t cb, void *priv, struct rpmsg_channel_info chinfo) { if (WARN_ON(!rpdev)) return NULL; return rpdev->ops->create_ept(rpdev, cb, priv, chinfo); } EXPORT_SYMBOL(rpmsg_create_ept); /** * rpmsg_destroy_ept() - destroy an existing rpmsg endpoint * @ept: endpoing to destroy * * Should be used by drivers to destroy an rpmsg endpoint previously * created with rpmsg_create_ept(). As with other types of "free" NULL * is a valid parameter. */ void rpmsg_destroy_ept(struct rpmsg_endpoint *ept) { if (ept) ept->ops->destroy_ept(ept); } EXPORT_SYMBOL(rpmsg_destroy_ept); /** * rpmsg_send() - send a message across to the remote processor * @ept: the rpmsg endpoint * @data: payload of message * @len: length of payload * * This function sends @data of length @len on the @ept endpoint. * The message will be sent to the remote processor which the @ept * endpoint belongs to, using @ept's address and its associated rpmsg * device destination addresses. * In case there are no TX buffers available, the function will block until * one becomes available, or a timeout of 15 seconds elapses. When the latter * happens, -ERESTARTSYS is returned. * * Can only be called from process context (for now). * * Returns 0 on success and an appropriate error value on failure. */ int rpmsg_send(struct rpmsg_endpoint *ept, void *data, int len) { if (WARN_ON(!ept)) return -EINVAL; if (!ept->ops->send) return -ENXIO; return ept->ops->send(ept, data, len); } EXPORT_SYMBOL(rpmsg_send); /** * rpmsg_sendto() - send a message across to the remote processor, specify dst * @ept: the rpmsg endpoint * @data: payload of message * @len: length of payload * @dst: destination address * * This function sends @data of length @len to the remote @dst address. * The message will be sent to the remote processor which the @ept * endpoint belongs to, using @ept's address as source. * In case there are no TX buffers available, the function will block until * one becomes available, or a timeout of 15 seconds elapses. When the latter * happens, -ERESTARTSYS is returned. * * Can only be called from process context (for now). * * Returns 0 on success and an appropriate error value on failure. */ int rpmsg_sendto(struct rpmsg_endpoint *ept, void *data, int len, u32 dst) { if (WARN_ON(!ept)) return -EINVAL; if (!ept->ops->sendto) return -ENXIO; return ept->ops->sendto(ept, data, len, dst); } EXPORT_SYMBOL(rpmsg_sendto); /** * rpmsg_send_offchannel() - send a message using explicit src/dst addresses * @ept: the rpmsg endpoint * @src: source address * @dst: destination address * @data: payload of message * @len: length of payload * * This function sends @data of length @len to the remote @dst address, * and uses @src as the source address. * The message will be sent to the remote processor which the @ept * endpoint belongs to. * In case there are no TX buffers available, the function will block until * one becomes available, or a timeout of 15 seconds elapses. When the latter * happens, -ERESTARTSYS is returned. * * Can only be called from process context (for now). * * Returns 0 on success and an appropriate error value on failure. */ int rpmsg_send_offchannel(struct rpmsg_endpoint *ept, u32 src, u32 dst, void *data, int len) { if (WARN_ON(!ept)) return -EINVAL; if (!ept->ops->send_offchannel) return -ENXIO; return ept->ops->send_offchannel(ept, src, dst, data, len); } EXPORT_SYMBOL(rpmsg_send_offchannel); /** * rpmsg_send() - send a message across to the remote processor * @ept: the rpmsg endpoint * @data: payload of message * @len: length of payload * * This function sends @data of length @len on the @ept endpoint. * The message will be sent to the remote processor which the @ept * endpoint belongs to, using @ept's address as source and its associated * rpdev's address as destination. * In case there are no TX buffers available, the function will immediately * return -ENOMEM without waiting until one becomes available. * * Can only be called from process context (for now). * * Returns 0 on success and an appropriate error value on failure. */ int rpmsg_trysend(struct rpmsg_endpoint *ept, void *data, int len) { if (WARN_ON(!ept)) return -EINVAL; if (!ept->ops->trysend) return -ENXIO; return ept->ops->trysend(ept, data, len); } EXPORT_SYMBOL(rpmsg_trysend); /** * rpmsg_sendto() - send a message across to the remote processor, specify dst * @ept: the rpmsg endpoint * @data: payload of message * @len: length of payload * @dst: destination address * * This function sends @data of length @len to the remote @dst address. * The message will be sent to the remote processor which the @ept * endpoint belongs to, using @ept's address as source. * In case there are no TX buffers available, the function will immediately * return -ENOMEM without waiting until one becomes available. * * Can only be called from process context (for now). * * Returns 0 on success and an appropriate error value on failure. */ int rpmsg_trysendto(struct rpmsg_endpoint *ept, void *data, int len, u32 dst) { if (WARN_ON(!ept)) return -EINVAL; if (!ept->ops->trysendto) return -ENXIO; return ept->ops->trysendto(ept, data, len, dst); } EXPORT_SYMBOL(rpmsg_trysendto); /** * rpmsg_poll() - poll the endpoint's send buffers * @ept: the rpmsg endpoint * @filp: file for poll_wait() * @wait: poll_table for poll_wait() * * Returns mask representing the current state of the endpoint's send buffers */ __poll_t rpmsg_poll(struct rpmsg_endpoint *ept, struct file *filp, poll_table *wait) { if (WARN_ON(!ept)) return 0; if (!ept->ops->poll) return 0; return ept->ops->poll(ept, filp, wait); } EXPORT_SYMBOL(rpmsg_poll); /** * rpmsg_send_offchannel() - send a message using explicit src/dst addresses * @ept: the rpmsg endpoint * @src: source address * @dst: destination address * @data: payload of message * @len: length of payload * * This function sends @data of length @len to the remote @dst address, * and uses @src as the source address. * The message will be sent to the remote processor which the @ept * endpoint belongs to. * In case there are no TX buffers available, the function will immediately * return -ENOMEM without waiting until one becomes available. * * Can only be called from process context (for now). * * Returns 0 on success and an appropriate error value on failure. */ int rpmsg_trysend_offchannel(struct rpmsg_endpoint *ept, u32 src, u32 dst, void *data, int len) { if (WARN_ON(!ept)) return -EINVAL; if (!ept->ops->trysend_offchannel) return -ENXIO; return ept->ops->trysend_offchannel(ept, src, dst, data, len); } EXPORT_SYMBOL(rpmsg_trysend_offchannel); 2、rpmsg_char.c的详细介绍

在这里插入图片描述

2.1 rpmsg_chrdev_driver结构体

定义如下:

static struct rpmsg_driver rpmsg_chrdev_driver = { .probe = rpmsg_chrdev_probe, .remove = rpmsg_chrdev_remove, .drv = { .name = "rpmsg_chrdev", }, };

从该结构体定义可以看出其是一个字符设备,在rpmsg_char_init()会register dirver

static int rpmsg_char_init(void) { int ret; /* 系统自动分配设备号 */ ret = alloc_chrdev_region(&rpmsg_major, 0, RPMSG_DEV_MAX, "rpmsg"); if (ret pr_err("failed to create rpmsg class\n"); unregister_chrdev_region(rpmsg_major, RPMSG_DEV_MAX); return PTR_ERR(rpmsg_class); } /* 注册rpmsg driver,实质上是赋值操作 */ ret = register_rpmsg_driver(&rpmsg_chrdev_driver); if (ret rpdrv->drv.bus = &rpmsg_bus; rpdrv->drv.owner = owner; return driver_register(&rpdrv->drv); } int driver_register(struct device_driver *drv) { int ret; struct device_driver *other; if (!drv->bus->p) { pr_err("Driver '%s' was unable to register with bus_type '%s' because the bus was not initialized.\n", drv->name, drv->bus->name); return -EINVAL; } /* 检查driver与bus的函数是否有冲突 */ if ((drv->bus->probe && drv->probe) || (drv->bus->remove && drv->remove) || (drv->bus->shutdown && drv->shutdown)) printk(KERN_WARNING "Driver '%s' needs updating - please use " "bus_type methods\n", drv->name); /* driver找到bus->p->drivers_kset中的空位(先会判断这个list中是否已经存在同名的driver) */ other = driver_find(drv->name, drv->bus); if (other) { printk(KERN_ERR "Error: Driver '%s' is already registered, " "aborting...\n", drv->name); return -EBUSY; } /* bus中添加driver */ ret = bus_add_driver(drv); if (ret) return ret; /* 如果grop不为空的话,将在驱动文件夹下创建以group名字的子文件夹,然后在子文件夹下添加group的属性文件 */ ret = driver_add_groups(drv, drv->groups); if (ret) { bus_remove_driver(drv); return ret; } kobject_uevent(&drv->p->kobj, KOBJ_ADD); return ret; }

而rpmsg_char_init会在驱动加载时被调用

postcore_initcall(rpmsg_char_init); 2.2 rpmsg_chrdev_probe()函数

问:该函数在何时被调用呢?

答:

在bus中,若driver,会触发bus中的bus_add_driver->driver_attach->__driver_attach>driver_probe_device->really_probe->

if (dev->bus->probe) { ret = dev->bus->probe(dev); if (ret) goto probe_failed; } else if (drv->probe) { ret = drv->probe(dev); if (ret) goto probe_failed; }

在bus中,若device add,会触发bus中device_add->bus_add_device,if(dev->bus)bus_probe_device->device_initial_probe->__device_attach->这里有两条路if(dev->driver) device_bind_driver, (走else)**else** **bus_for_each_drv(dev->bus, NULL, &data,__device_attach_driver)**;->driver_match_device->driver_probe_device->>really_probe->

if (dev->bus->probe) { ret = dev->bus->probe(dev); if (ret) goto probe_failed; } else if (drv->probe) { ret = drv->probe(dev); if (ret) goto probe_failed; }

从上面分析可以看到bus->probe优先级比driver->probe高。

问:那是不是表示rpmsg driver->probe永远也不会被调用到?

答:不会,应为rpmsg bus->probe()中会调用rpmsg driver->probe;

接下来分析rpmsg_chrdev_probe()函数中主要做的事情。

前提:刚刚分析了,只有在driver match device后才会调用到rpmsg bus probe进而调用到rpmsg driver probe,故此时对应的device已经找到了。 static int rpmsg_chrdev_probe(struct rpmsg_device *rpdev) { struct rpmsg_ctrldev *ctrldev; struct device *dev; int ret; /* 初始化rpmsg 控制设备,该控制设备保存着已经被实例化的ept device*/ ctrldev = kzalloc(sizeof(*ctrldev), GFP_KERNEL); if (!ctrldev) return -ENOMEM; ctrldev->rpdev = rpdev; dev = &ctrldev->dev; device_initialize(dev); dev->parent = &rpdev->dev; dev->class = rpmsg_class; /* 为该控制设备添加字符设备,以便后续支持ioctl */ cdev_init(&ctrldev->cdev, &rpmsg_ctrldev_fops); ctrldev->cdev.owner = THIS_MODULE; /* 获取ida数组中一个有效的index,并获取此设备号 */ ret = ida_simple_get(&rpmsg_minor_ida, 0, RPMSG_DEV_MAX, GFP_KERNEL); if (ret devt = MKDEV(MAJOR(rpmsg_major), ret); /* 获取ida数组中一个有效的index,并设置名字 */ ret = ida_simple_get(&rpmsg_ctrl_ida, 0, 0, GFP_KERNEL); if (ret id = ret; dev_set_name(&ctrldev->dev, "rpmsg_ctrl%d", ret); /* 填充设备号 */ ret = cdev_add(&ctrldev->cdev, dev->devt, 1); if (ret) goto free_ctrl_ida; /* We can now rely on the release function for cleanup */ dev->release = rpmsg_ctrldev_release_device; /* 添加字符设备,后续可以通过dev找到所对应的rpmsg_ctrldev,进而可以会找到rpmsg device */ ret = device_add(dev); if (ret) { dev_err(&rpdev->dev, "device_add failed: %d\n", ret); put_device(dev); } dev_set_drvdata(&rpdev->dev, ctrldev); return ret; free_ctrl_ida: ida_simple_remove(&rpmsg_ctrl_ida, dev->id); free_minor_ida: ida_simple_remove(&rpmsg_minor_ida, MINOR(dev->devt)); free_ctrldev: put_device(dev); kfree(ctrldev); return ret; } 2.3 rpmsg_chrdev_remove()函数

从下面的函数可以知道,当该driver remove,则改在在该driver下的所有rpmsg device会一个个被调用rpmsg_eptdev_destroy-> ept->ops->destroy_ept,可以看到每个rpmsg device维护自己的device函数。

那么这里留一个问题,什么时候rpmsg_create_ept?

static void rpmsg_chrdev_remove(struct rpmsg_device *rpdev) { struct rpmsg_ctrldev *ctrldev = dev_get_drvdata(&rpdev->dev); int ret; /* Destroy all endpoints */ ret = device_for_each_child(&ctrldev->dev, NULL, rpmsg_eptdev_destroy); if (ret) dev_warn(&rpdev->dev, "failed to nuke endpoints: %d\n", ret); device_del(&ctrldev->dev); put_device(&ctrldev->dev); } 二、virtio_rpmsg_bus.c 的详细介绍

在这里插入图片描述

1、virtio_ipc_driver 结构体的介绍 static struct virtio_device_id id_table[] = { { VIRTIO_ID_RPMSG, VIRTIO_DEV_ANY_ID }, { 0 }, }; static unsigned int features[] = { VIRTIO_RPMSG_F_NS, }; static struct virtio_driver virtio_ipc_driver = { .feature_table = features, .feature_table_size = ARRAY_SIZE(features), .driver.name = KBUILD_MODNAME, .driver.owner = THIS_MODULE, .id_table = id_table, .probe = rpmsg_probe, .remove = rpmsg_remove, };

从上面结构体中的变量可以看到,该virtio_dirver有属于自己的probo与remove函数,故可以猜测该dirver是属于virtio_bus的(该猜测在后续中会印证)。

该结构体会在 subsys_initcall(rpmsg_init); --> register_virtio_driver(&virtio_ipc_driver);中被注册,通过查看register_virtio_dirver函数:

该virtio_driver被设为的属于virtio_bus —>这就印证了刚刚的猜测;通过driver_register将virtio_driver register 到 virtio_bus中; int register_virtio_driver(struct virtio_driver *driver) { /* Catch this early. */ BUG_ON(driver->feature_table_size && !driver->feature_table); driver->driver.bus = &virtio_bus; return driver_register(&driver->driver); } 2、rpmsg_probe()函数的介绍

注意该函数是virtio_driver中的probe函数。

源码如下图:

主要做了如下的事情:

根据vdev->config->find_vqs(),根据names作为匹配原则期望找到对应的rx与tx virtqueue;

根据找到的rx/tx,将其赋值到virtproc_info->virqueue[0/1] ;

统计rx/tx 所需要的buf size/buf num/total buf space,并为需要的buf申请DMA memory;

DMA memory对半分给若rx/tx;

对每一个rx buf进行vring的初始化;

将上述virtproc_info初始化赋值给该virtio_device;

为支持remote process,创建一个以RPMSG_NS_ADDR的virtproc_info->ns_ept,支持后续的name service announcement;

准备kick_off并告知remote process notify

在该函数中涉及到很多数据结构,统一的来说:

先根据名字,找到rx/tx virtqueue(vqs);将rx/tx virtqueue存储到virtproc_info(vrq)->rvq/tvq;初始化vrq(rvq vring、tvq info、ns_ept);将vrq存储到virtio_device中; static int rpmsg_probe(struct virtio_device *vdev) { vq_callback_t *vq_cbs[] = { rpmsg_recv_done, rpmsg_xmit_done }; static const char * const names[] = { "input", "output" }; struct virtqueue *vqs[2]; struct virtproc_info *vrp; void *bufs_va; int err = 0, i; size_t total_buf_space; bool notify; vrp = kzalloc(sizeof(*vrp), GFP_KERNEL); if (!vrp) return -ENOMEM; vrp->vdev = vdev; idr_init(&vrp->endpoints); mutex_init(&vrp->endpoints_lock); mutex_init(&vrp->tx_lock); init_waitqueue_head(&vrp->sendq); /* 根据vdev->config->find_vqs(),根据names作为匹配原则期望找到对应的rx与tx * 其中rx与tx对应各自的virtqueue */ err = virtio_find_vqs(vdev, 2, vqs, vq_cbs, names, NULL); if (err) goto free_vrp; /* 根据找到的rx/tx,将其赋值到virtproc_info->virqueue[0/1] */ vrp->rvq = vqs[0]; vrp->svq = vqs[1]; /* 期望rx/tx virtqueue对应的vring size是对称的 */ WARN_ON(virtqueue_get_vring_size(vrp->rvq) != virtqueue_get_vring_size(vrp->svq)); /* 如果rx vring size小于MAX_RPMSG_NUM_BUFS / 2,则后续可利用的buf num为最小size * 2 * 否则以最大的MAX_RPMS_NUM_BUFS为准 */ if (virtqueue_get_vring_size(vrp->rvq) num_bufs = virtqueue_get_vring_size(vrp->rvq) * 2; else vrp->num_bufs = MAX_RPMSG_NUM_BUFS; vrp->buf_size = MAX_RPMSG_BUF_SIZE; total_buf_space = vrp->num_bufs * vrp->buf_size; /* 根据统计所得的buf space 申请一段连续的DMA memory */ bufs_va = dma_alloc_coherent(vdev->dev.parent->parent, total_buf_space, &vrp->bufs_dma, GFP_KERNEL); if (!bufs_va) { err = -ENOMEM; goto vqs_del; } dev_dbg(&vdev->dev, "buffers: va %p, dma %pad\n", bufs_va, &vrp->bufs_dma); /* 将上述的DMA内存对半给rx与tx的buf, DMA memory的起始地址在rx */ vrp->rbufs = bufs_va; vrp->sbufs = bufs_va + total_buf_space / 2; /* 根据rx buf num对每一个buf进行初始化 */ for (i = 0; i num_bufs / 2; i++) { struct scatterlist sg; void *cpu_addr = vrp->rbufs + i * vrp->buf_size; //获取每一个rx buf cpu addr rpmsg_sg_init(&sg, cpu_addr, vrp->buf_size); //根据cpu addr初始化一个散列表 err = virtqueue_add_inbuf(vrp->rvq, &sg, 1, cpu_addr, //根据散列表与cpu addr,初始化rx vring inbuf GFP_KERNEL); WARN_ON(err); /* sanity check; this can't really happen */ } /* 设置相关的tx flag,来抑制发送行为 */ virtqueue_disable_cb(vrp->svq); /* 将virtproc_info赋值给该virtio_device */ vdev->priv = vrp; /* 创建一个RPMSG_NS_ADDR的ept,以供支持remote process */ if (virtio_has_feature(vdev, VIRTIO_RPMSG_F_NS)) { /* a dedicated endpoint handles the name service msgs */ vrp->ns_ept = __rpmsg_create_ept(vrp, NULL, rpmsg_ns_cb, vrp, RPMSG_NS_ADDR); if (!vrp->ns_ept) { dev_err(&vdev->dev, "failed to create the ns ept\n"); err = -ENOMEM; goto free_coherent; } } /* 准备事件相关的kick off(中断、标志) */ notify = virtqueue_kick_prepare(vrp->rvq); /* 将该virtio device设置成ready status. */ virtio_device_ready(vdev); /* 告知remote device可以发消息了,notify一般为戳中断的形式 */ if (notify) virtqueue_notify(vrp->rvq); dev_info(&vdev->dev, "rpmsg host is online\n"); return 0; free_coherent: dma_free_coherent(vdev->dev.parent->parent, total_buf_space, bufs_va, vrp->bufs_dma); vqs_del: vdev->config->del_vqs(vrp->vdev); free_vrp: kfree(vrp); return err; } 3、__rpmsg_create_ept()介绍

其主要将该ns_ept的ops = virtio_endpoint_ops,从上面的函数调用可以知道,传进去的rpdev == NULL,所以这个函数还未与rpmsg_bus建立联系。

问:何时才与rpmsg _bus建立联系呢?答:(vrp->ns_ept->cb == rpmsg_ns_cb)–>rpmsg_create_channel–>rpmsg_register_device();问:何时才调用rpmsg_ns_cb?答:从之前的分析,都是对virtio_driver结构体的分析,应该在后续有virtio_device match时候,调用到virtio_bus->probe --> virtio_driver->probe -->为这个virtio_device 进行初始化操作,此时virtio_device->priv->ns_ept = ept,其中ept->rpdev == NULL,后续可使用这个virtio_device 进行name service rpmsg_create_channel->rpmsg_register_device,进行rpmsg_device的真正建立。进而virtio_device对应的virtio_driver有真正的rpdev,使得virtio_driver、virtio_device通过rpmsg_device与rpmsg_bus打交道。 static struct rpmsg_endpoint *__rpmsg_create_ept(struct virtproc_info *vrp, struct rpmsg_device *rpdev, rpmsg_rx_cb_t cb, void *priv, u32 addr) { int id_min, id_max, id; struct rpmsg_endpoint *ept; struct device *dev = rpdev ? &rpdev->dev : &vrp->vdev->dev; ept = kzalloc(sizeof(*ept), GFP_KERNEL); if (!ept) return NULL; kref_init(&ept->refcount); mutex_init(&ept->cb_lock); /* 初始化ept */ ept->rpdev = rpdev; ept->cb = cb; ept->priv = priv; ept->ops = &virtio_endpoint_ops; /* 该virproc_info->ns_ept->ops = virtio_endpoint_ops */ /* do we need to allocate a local address ? */ if (addr == RPMSG_ADDR_ANY) { id_min = RPMSG_RESERVED_ADDRESSES; id_max = 0; } else { id_min = addr; id_max = addr + 1; } mutex_lock(&vrp->endpoints_lock); /* bind the endpoint to an rpmsg address (and allocate one if needed) */ id = idr_alloc(&vrp->endpoints, ept, id_min, id_max, GFP_KERNEL); if (id .destroy_ept = virtio_rpmsg_destroy_ept, .send = virtio_rpmsg_send, .sendto = virtio_rpmsg_sendto, .send_offchannel = virtio_rpmsg_send_offchannel, .trysend = virtio_rpmsg_trysend, .trysendto = virtio_rpmsg_trysendto, .trysend_offchannel = virtio_rpmsg_trysend_offchannel, }; 5、rpmsg_ns_cb() 函数的介绍

在rpsmg_probe()函数中,为了支持remote_device,会进行virtio_device->priv(virtproc)->ns_ept = cretea ns_ept ,其中就会为注册rpmsg_ns_cb(),该函数主要进行如下事情:

初始化rpmsg_channel_info;根据channel_info进行rpomsg_create_channel static int rpmsg_ns_cb(struct rpmsg_device *rpdev, void *data, int len, void *priv, u32 src) { struct rpmsg_ns_msg *msg = data; struct rpmsg_device *newch; struct rpmsg_channel_info chinfo; struct virtproc_info *vrp = priv; struct device *dev = &vrp->vdev->dev; int ret; #if defined(CONFIG_DYNAMIC_DEBUG) dynamic_hex_dump("NS announcement: ", DUMP_PREFIX_NONE, 16, 1, data, len, true); #endif if (len != sizeof(*msg)) { dev_err(dev, "malformed ns msg (%d)\n", len); return -EINVAL; } /* * the name service ept does _not_ belong to a real rpmsg channel, * and is handled by the rpmsg bus itself. * for sanity reasons, make sure a valid rpdev has _not_ sneaked * in somehow. */ if (rpdev) { dev_err(dev, "anomaly: ns ept has an rpdev handle\n"); return -EINVAL; } /* don't trust the remote processor for null terminating the name */ msg->name[RPMSG_NAME_SIZE - 1] = '\0'; dev_info(dev, "%sing channel %s addr 0x%x\n", msg->flags & RPMSG_NS_DESTROY ? "destroy" : "creat", msg->name, msg->addr); strncpy(chinfo.name, msg->name, sizeof(chinfo.name)); chinfo.src = RPMSG_ADDR_ANY; chinfo.dst = msg->addr; if (msg->flags & RPMSG_NS_DESTROY) { ret = rpmsg_unregister_device(&vrp->vdev->dev, &chinfo); if (ret) dev_err(dev, "rpmsg_destroy_channel failed: %d\n", ret); } else { newch = rpmsg_create_channel(vrp, &chinfo); if (!newch) dev_err(dev, "rpmsg_create_channel failed\n"); } return 0; } 6、rpmsg_create_channel() 函数的介绍

函数原型如下:

调用rpmsg_device_match,根据chinfo来确认该channel没有被创建 ;分配virtio_rpmsg_channel memory;初始化rpmsg-device:virtio_rpmsg_channel->rpmsg_device src、dst、ops、announce、id.name;根据初始化的rpmsg_device,进行rpmsg_register_device,往rpmsg_bus中register device;返回rpmsg_device,该rpmsg_device addr == virtio_rpmsg_channel->rpmsg_device;

这样就可以理解为:

一个virtio_device对应一个virtio_rpmsg_channel;一个virtio_rpmsg_channel对应一个rpmsg_device;一个rpmsg_device对应一个rpmsg_driver; /* * create an rpmsg channel using its name and address info. * this function will be used to create both static and dynamic * channels. */ static struct rpmsg_device *rpmsg_create_channel(struct virtproc_info *vrp, struct rpmsg_channel_info *chinfo) { struct virtio_rpmsg_channel *vch; struct rpmsg_device *rpdev; struct device *tmp, *dev = &vrp->vdev->dev; int ret; /* make sure a similar channel doesn't already exist */ /* 调用rpmsg_device_match,根据chinfo来确认该channel没有被创建 */ tmp = rpmsg_find_device(dev, chinfo); if (tmp) { /* decrement the matched device's refcount back */ put_device(tmp); dev_err(dev, "channel %s:%x:%x already exist\n", chinfo->name, chinfo->src, chinfo->dst); return NULL; } /* 分配virtio_rpmsg_channel memory */ vch = kzalloc(sizeof(*vch), GFP_KERNEL); if (!vch) return NULL; /* Link the channel to our vrp */ vch->vrp = vrp; /* Assign public information to the rpmsg_device */ /* 初始化rpmsg-device * virtio_rpmsg_channel->rpmsg_device src、dst、ops、announce、id.name */ rpdev = &vch->rpdev; rpdev->src = chinfo->src; rpdev->dst = chinfo->dst; rpdev->ops = &virtio_rpmsg_ops; /* * rpmsg server channels has predefined local address (for now), * and their existence needs to be announced remotely */ rpdev->announce = rpdev->src != RPMSG_ADDR_ANY; strncpy(rpdev->id.name, chinfo->name, RPMSG_NAME_SIZE); rpdev->dev.parent = &vrp->vdev->dev; rpdev->dev.release = virtio_rpmsg_release_device; /* 根据初始化的rpmsg_device,进行该device的register */ ret = rpmsg_register_device(rpdev); if (ret) return NULL; return rpdev; } 7、virtio_rpmsg_ops() 结构体的介绍

上面提到过,会将rpmsg_device->ops = &virtio_rpmsg_ops,这个结构体里面有如下三个操作函数,结合上面的理解,可以进一步规划为:

一个virtio_device对应一个virtio_rpmsg_channel;一个virtio_rpmsg_channel对应一个rpmsg_device,一个virtio_rpmsg_channel对应多个rpmsg_endpoint;一个rpmsg_device对应一个rpmsg_driver; static const struct rpmsg_device_ops virtio_rpmsg_ops = { .create_ept = virtio_rpmsg_create_ept, .announce_create = virtio_rpmsg_announce_create, .announce_destroy = virtio_rpmsg_announce_destroy, };

其中virtio_rpmsg_create_ept()最终是调用__rpmsg_create_ept()。

三、Virtio的介绍 1、virtio.c 的介绍

在这里插入图片描述

1.1 virtio_bus结构体的介绍

源码如下:

该文件会建立一个name为“virtio”的bus

有几个比较关键的函数:

virtio_dev_match;virtio_dev_groups;virtio_dev_probe;virtio_dev_remove; static struct bus_type virtio_bus = { .name = "virtio", .match = virtio_dev_match, .dev_groups = virtio_dev_groups, .uevent = virtio_uevent, .probe = virtio_dev_probe, .remove = virtio_dev_remove, };

其通过core_initcall和module_exit进行bus的初始化

static int virtio_init(void) { if (bus_register(&virtio_bus) != 0) panic("virtio bus registration failed"); return 0; } static void __exit virtio_exit(void) { bus_unregister(&virtio_bus); ida_destroy(&virtio_index_ida); } core_initcall(virtio_init); module_exit(virtio_exit); 1.2 register_virtio_driver() 函数的介绍

当我们需要向virtio_bus register一个virtio_driver就需要调用该函数。

源码如下:可以看到其最终是调用driver_register

int register_virtio_driver(struct virtio_driver *driver) { /* Catch this early. */ BUG_ON(driver->feature_table_size && !driver->feature_table); driver->driver.bus = &virtio_bus; return driver_register(&driver->driver); } EXPORT_SYMBOL_GPL(register_virtio_driver); 1.3 register_virtio_device() 函数的介绍

当我们需要向virtio_bus register一个virtio_device就需要调用该函数。

源码如下:可以看到其最终是调用device_add(),主要做了如下事情:

bus name 赋值;设置相关标志;调用device_add()向bus总线添加device; /** * register_virtio_device - register virtio device * @dev : virtio device to be registered * * On error, the caller must call put_device on &@dev->dev (and not kfree), * as another code path may have obtained a reference to @dev. * * Returns: 0 on suceess, -error on failure */ int register_virtio_device(struct virtio_device *dev) { int err; dev->dev.bus = &virtio_bus; device_initialize(&dev->dev); /* Assign a unique device index and hence name. */ err = ida_simple_get(&virtio_index_ida, 0, 0, GFP_KERNEL); if (err index = err; dev_set_name(&dev->dev, "virtio%u", dev->index); spin_lock_init(&dev->config_lock); dev->config_enabled = false; dev->config_change_pending = false; /* We always start by resetting the device, in case a previous * driver messed it up. This also tests that code path a little. */ dev->config->reset(dev); /* Acknowledge that we've seen the device. */ virtio_add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE); INIT_LIST_HEAD(&dev->vqs); /* * device_add() causes the bus infrastructure to look for a matching * driver. */ err = device_add(&dev->dev); if (err) ida_simple_remove(&virtio_index_ida, dev->index); out: if (err) virtio_add_status(dev, VIRTIO_CONFIG_S_FAILED); return err; } EXPORT_SYMBOL_GPL(register_virtio_device); 1.4 virtio_dev_probe 函数的介绍

该函数主要做如下事情:

根据据传入的device,找该device说属于的virtio_device和virtio_driver;设定相关的标志位;**调用virtio_driver->probe()**这个函数后面会进行具体分析;完成配置;

函数源码:

static int virtio_dev_probe(struct device *_d) { int err, i; /* find virtio_device according to device */ struct virtio_device *dev = dev_to_virtio(_d); /* find virtio_driver according to virtio_device */ struct virtio_driver *drv = drv_to_virtio(dev->dev.driver); u64 device_features; u64 driver_features; u64 driver_features_legacy; /* We have a driver! */ virtio_add_status(dev, VIRTIO_CONFIG_S_DRIVER); /* Figure out what features the device supports. */ device_features = dev->config->get_features(dev); /* Figure out what features the driver supports. */ driver_features = 0; for (i = 0; i feature_table_size; i++) { unsigned int f = drv->feature_table[i]; BUG_ON(f >= 64); driver_features |= (1ULL unsigned int f = drv->feature_table_legacy[i]; BUG_ON(f >= 64); driver_features_legacy |= (1ULL err = drv->validate(dev); if (err) goto err; } err = virtio_finalize_features(dev); if (err) goto err; /* callback virtio_driver->probe() */ err = drv->probe(dev); if (err) goto err; /* If probe didn't do it, mark device DRIVER_OK ourselves. */ if (!(dev->config->get_status(dev) & VIRTIO_CONFIG_S_DRIVER_OK)) virtio_device_ready(dev); if (drv->scan) drv->scan(dev); virtio_config_enable(dev); return 0; err: virtio_add_status(dev, VIRTIO_CONFIG_S_FAILED); return err; } 1.5 virtio_dev_match 函数的介绍

该函数是virtio_bus进行device 和driver match

源码如下:

可以看到,其有两个匹配原则:

driver匹配任意device:virtio_driver->id_table[i] (virtio_device_id) ->vendor == VIRTIO_DEV_ANY_ID;driver匹配指定一类device:virtio_driver->id_table[i] (virtio_device_id) ->vendor == virtio_device->id.vendor static inline int virtio_id_match(const struct virtio_device *dev, const struct virtio_device_id *id) { if (id->device != dev->id.device && id->device != VIRTIO_DEV_ANY_ID) return 0; return id->vendor == VIRTIO_DEV_ANY_ID || id->vendor == dev->id.vendor; } /* This looks through all the IDs a driver claims to support. If any of them * match, we return 1 and the kernel will call virtio_dev_probe(). */ static int virtio_dev_match(struct device *_dv, struct device_driver *_dr) { unsigned int i; struct virtio_device *dev = dev_to_virtio(_dv); const struct virtio_device_id *ids; ids = drv_to_virtio(_dr)->id_table; for (i = 0; ids[i].device; i++) if (virtio_id_match(dev, &ids[i])) return 1; return 0; } 2、virtio_input.c的介绍

上面virtio.c是介绍virtio bus,该文件是介绍virtio_driver。

在整个目前所使用的kernel版本中,有多种virtio_driver(virtio_pci_driver、virtio-mmio、virtio_balloon_driver、virtio_input_driver),除了virtio_input_driver支持VIRTIO_DEV_ANY_ID,其他的virtio_driver都是支持指定的virtio_device。

2.1 virtio_input_driver结构体介绍

原型如下:

可以看到其支持power manager的相关freeze和restore操作,当然其最重要的函数就是virtinput_probe;这个结构体是通过module_virtio_driver宏进而调用register_virtio_driver() static unsigned int features[] = { /* none */ }; static struct virtio_device_id id_table[] = { { VIRTIO_ID_INPUT, VIRTIO_DEV_ANY_ID }, { 0 }, }; static struct virtio_driver virtio_input_driver = { .driver.name = KBUILD_MODNAME, .driver.owner = THIS_MODULE, .feature_table = features, .feature_table_size = ARRAY_SIZE(features), .id_table = id_table, .probe = virtinput_probe, .remove = virtinput_remove, #ifdef CONFIG_PM_SLEEP .freeze = virtinput_freeze, .restore = virtinput_restore, #endif }; module_virtio_driver(virtio_input_driver);

2.2 virtinput_probe() 函数的介绍

函数原型如下:可以看到所做的事情和virtio_ipc_driver是类似的:

初始化virtioquqe,设置callback;分配buf;设置相关属性:name、physaddr、serial name;设置device为ready状态 ;kick off; static int virtinput_probe(struct virtio_device *vdev) { struct virtio_input *vi; unsigned long flags; size_t size; int abs, err; if (!virtio_has_feature(vdev, VIRTIO_F_VERSION_1)) return -ENODEV; vi = kzalloc(sizeof(*vi), GFP_KERNEL); if (!vi) return -ENOMEM; vdev->priv = vi; vi->vdev = vdev; spin_lock_init(&vi->lock); /* 初始化virtqueue * callback virtio_device->config->find_vqs * init virtinput_recv_events and virtinput_recv_status callback */ err = virtinput_init_vqs(vi); if (err) goto err_init_vq; /* alloc buf for device */ vi->idev = input_allocate_device(); if (!vi->idev) { err = -ENOMEM; goto err_input_alloc; } input_set_drvdata(vi->idev, vi); /* 设置相关属性:name、physaddr、serial name*/ size = virtinput_cfg_select(vi, VIRTIO_INPUT_CFG_ID_NAME, 0); virtio_cread_bytes(vi->vdev, offsetof(struct virtio_input_config, u.string), vi->name, min(size, sizeof(vi->name))); size = virtinput_cfg_select(vi, VIRTIO_INPUT_CFG_ID_SERIAL, 0); virtio_cread_bytes(vi->vdev, offsetof(struct virtio_input_config, u.string), vi->serial, min(size, sizeof(vi->serial))); snprintf(vi->phys, sizeof(vi->phys), "virtio%d/input0", vdev->index); vi->idev->name = vi->name; vi->idev->phys = vi->phys; vi->idev->uniq = vi->serial; size = virtinput_cfg_select(vi, VIRTIO_INPUT_CFG_ID_DEVIDS, 0); if (size >= sizeof(struct virtio_input_devids)) { virtio_cread(vi->vdev, struct virtio_input_config, u.ids.bustype, &vi->idev->id.bustype); virtio_cread(vi->vdev, struct virtio_input_config, u.ids.vendor, &vi->idev->id.vendor); virtio_cread(vi->vdev, struct virtio_input_config, u.ids.product, &vi->idev->id.product); virtio_cread(vi->vdev, struct virtio_input_config, u.ids.version, &vi->idev->id.version); } else { vi->idev->id.bustype = BUS_VIRTUAL; } virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_PROP_BITS, 0, vi->idev->propbit, INPUT_PROP_CNT); size = virtinput_cfg_select(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_REP); if (size) __set_bit(EV_REP, vi->idev->evbit); vi->idev->dev.parent = &vdev->dev; vi->idev->event = virtinput_status; /* device -> kernel */ virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_KEY, vi->idev->keybit, KEY_CNT); virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_REL, vi->idev->relbit, REL_CNT); virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_ABS, vi->idev->absbit, ABS_CNT); virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_MSC, vi->idev->mscbit, MSC_CNT); virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_SW, vi->idev->swbit, SW_CNT); /* kernel -> device */ virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_LED, vi->idev->ledbit, LED_CNT); virtinput_cfg_bits(vi, VIRTIO_INPUT_CFG_EV_BITS, EV_SND, vi->idev->sndbit, SND_CNT); if (test_bit(EV_ABS, vi->idev->evbit)) { for (abs = 0; abs rpmsg_device----rpmsg_bus-----rpmsg_driver,之间的联系,如下图: 在这里插入图片描述

2、运作

由于此时已经建立的联系,可以使用rpmsg_create_ept、rpmsg_send、rpmsg_trysend、rpmsg_poll等一系列的rpmsg operation,这个会进而会以callback的形式,最终调用virtio_rpmsg_bus.c中的virtio_rpmsg_ops。

举个例子,调用rpsmg_create_ept()

/----------------- rpmsg_core.c ---------------------/ struct rpmsg_endpoint *rpmsg_create_ept(struct rpmsg_device *rpdev, rpmsg_rx_cb_t cb, void *priv, struct rpmsg_channel_info chinfo) { if (WARN_ON(!rpdev)) return NULL; return rpdev->ops->create_ept(rpdev, cb, priv, chinfo); } /* 该rpmsg device->ops,即是之前 * virtio_device进行virtio_ipc_driver->probe()时: * virtio_device->priv(virtproc_info)->ns_ept->ops = & virtio_endpoint_ops; * 当根据该ns_ept->cb()进行rpmsg_channel-->rpmsg_device的建立时: * rpmsg->device->ops = & virtio_rpmsg_ops * virtio_rpmsg_ops中就有virtio_rpmsg_create_ept */


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭